﻿////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright file="AbstractModCollection.cs" company="SingularityShift.com">
// Copyright (c) 2010 SingularityShift.com. All rights reserved.
// </copyright>
// <author>Ben McClure</author>
// <email>ben@singularityshift.com</email>
// <date>11/24/2010</date>
// <summary>Implements the abstract mod collection class</summary>
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace GameLib.Games
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Text.RegularExpressions;
    using System.Xml.Serialization;
    using GameLib.Games.Components;
    using GameLib.Games.Util;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    /// <summary>   Collection of abstract mods. </summary>
    ///
    /// <remarks>   Ben McClure, 11/24/2010. </remarks>
    ////////////////////////////////////////////////////////////////////////////////////////////////////
    public abstract class AbstractModCollection<T> : BindingList<T>, IModCollection<T> where T : IMod
    {
        /// <summary>
        /// Thrown whenever the ModList has been imported into.
        /// </summary>
        public event EventHandler<EventArgs> ModListImported;

        /// <summary>
        /// Thrown whenever the ModList has been exported from.
        /// </summary>
        public event EventHandler<EventArgs> ModListExported;

        /// <summary>
        /// Gets or sets the game which owns (is the parent of) this list. The game should also own all mods within the list.
        /// </summary>
        public IGame<T> Owner
        {
            get;
            protected set;
        }

        /// <summary>
        /// Gets or sets the mods in the collection as an Array of IMod objects.
        /// </summary>
        [XmlArray()]
        public T[] ModArray
        {
            get
            {
                return this.ToArray();
            }

            protected set
            {
                this.Clear();
                foreach (T mod in value)
                {
                    this.Add(mod);
                }
            }
        }

        /// <summary>
        /// Gets or sets the SortType enum that defines how this list should be sorted.
        /// </summary>
        public ModSortTypes SortBy
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the DateTime object indicating when this list was created.
        /// </summary>
        public DateTime Created
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the IMod specified by the provided Id string. This requires no casting.
        /// </summary>
        /// <param name="id">The Mod Id to look up</param>
        public T this[string id]
        {
            get
            {
                IList<T> found = this.Find("Id", id);
                if (found.Count > 0)
                {
                    return found[0];
                }
                else
                {
                    return default(T);
                }
            }

            set
            {
                IList<T> found = this.Find("Id", id);
                foreach (T mod in found)
                {
                    this.Remove(mod);
                }

                value.Id = id;
                this.Add(value);
            }
        }

        /// <summary>
        /// Exports the Ids of the IMod objects in the list to a text file.
        /// </summary>
        /// <param name="textFile">The file to output the list objects to.</param>
        /// <returns>An int specifying the number of objects exported</returns>
        public virtual int Export(string textFile)
        {
            return this.Export(textFile, false);
        }

        /// <summary>
        /// Exports the Ids of the IMod objects in the list to a text file.
        /// </summary>
        /// <param name="textFile">The file to output the list objects to.</param>
        /// <param name="overwrite">Whether or not to overwrite the text file if it exists.</param>
        /// <returns>An int specifying the number of objects exported</returns>
        public virtual int Export(string textFile, bool overwrite)
        {
            if ((!overwrite) && File.Exists(textFile))
            {
                throw new IOException(textFile + " exists and was not overwritten");
            }

            int exported = 0;
            bool append = false;
            append = overwrite ? false : true;
            using (StreamWriter sw = new StreamWriter(textFile, append))
            {
                using (TextWriter output = TextWriter.Synchronized(sw))
                {
                    foreach (T mod in this)
                    {
                        if (exported > 0)
                        {
                            output.WriteLine();
                        }

                        output.Write(mod.Id);
                        exported++;
                    }
                }
            }

            this.OnModListExported(new EventArgs());
            return exported;
        }

        /// <summary>
        /// Finds all IMod objects in the list where the specified field matches the provided value.
        /// </summary>
        /// <param name="field">The field within each IMod object to search in</param>
        /// <param name="value">The value to match against the field</param>
        /// <returns>An IList of IMod objects that match the specified query</returns>
        public virtual IModCollection<T> Find(string field, object value)
        {
            return this.Find(field, value, false);
        }

        /// <summary>
        /// Finds all IMod objects in the list where the specified field matches the provided value, optionally supporting regular expressions.
        /// </summary>
        /// <param name="field">The field within each IMod object to search in</param>
        /// <param name="value">The value to match against the field</param>
        /// <param name="regex">A bool indicating whether the search value is a RegEx pattern or not</param>
        /// <returns>An IList of IMod objects that match the specified query</returns>
        public virtual IModCollection<T> Find(string field, object value, bool regex)
        {
            IModCollection<T> result = new ModCollection<T>(this.Owner);

            Regex rx = null;
            if (regex)
            {
                rx = new Regex(@value.ToString());
            }

            foreach (T mod in this)
            {
                Type type = mod.GetType();
                PropertyInfo fieldInfo = type.GetProperty(field);
                if (fieldInfo == null)
                {
                    throw new MemberAccessException("Cannot locate field " + field);
                }

                if (regex)
                {
                    if (rx.IsMatch(fieldInfo.GetValue(mod, null).ToString()))
                    {
                        result.Add(mod);
                    }
                }
                else if (value.Equals(fieldInfo.GetValue(mod, null)))
                {
                    result.Add(mod);
                }
            }

            return result;
        }

        /// <summary>
        /// <para>Clears the list and uses the IMod objects in the provided array to repopulate it.</para>
        /// <para>This will clear any previous IMods from the list.</para>
        /// </summary>
        /// <param name="mods">The array of IMod objects to turn the list into.</param>
        public void FromArray(T[] mods)
        {
            this.Clear();
            foreach (T mod in mods)
            {
                this.Add(mod);
            }
        }

        /// <summary>
        /// Indicates whether any mod in the list conflicts with any other mod in the list.
        /// </summary>
        /// <returns>A boolean indicating whether the list has any conflicts.</returns>
        public bool HasMatchingConflicts()
        {
            foreach (T mod in this)
            {
                if (mod.HasMatchingConflicts((IModCollection<T>)this))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Indicates whether any mod in the list is has any dependency found in the list.
        /// </summary>
        /// <returns>A boolean indicating if there are matching dependencies in the list.</returns>
        public bool HasMatchingDependencies()
        {
            foreach (T mod in this)
            {
                if (mod.HasMatchingDependencies((IModCollection<T>)this))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Indicates whether any mod in the list is has any dependency not found in the list.
        /// </summary>
        /// <returns>A boolean indicating if there are missing dependencies in the list.</returns>
        public bool HasMissingDependencies()
        {
            foreach (T mod in this)
            {
                if (mod.HasMissingDependencies((IModCollection<T>)this))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Indicates whether any mod in the list has messages matching any other mod in the list.
        /// </summary>
        /// <returns>A boolean indicating if there are matching messages in the list.</returns>
        public bool HasMatchingMessages()
        {
            foreach (T mod in this)
            {
                if (mod.HasMatchingMessages((IModCollection<T>)this))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Imports IMod objects from a text file containing a list of Ids.
        /// </summary>
        /// <param name="textFile">The text file to import. This should contain a delimited list of mod Ids.</param>
        /// <returns>An integer indicating the number of mods imported.</returns>
        public virtual int Import(string textFile)
        {
            return this.Import(textFile, ModSortTypes.ActualOrder, null);
        }

        /// <summary>
        /// Imports IMod objects from a text file containing a list of Ids.
        /// </summary>
        /// <param name="textFile">The text file to import. This should contain a delimited list of mod Ids.</param>
        /// <param name="sortBy">A SortType enum describing how to sort the imported mods in the list.</param>
        /// <returns>An integer indicating the number of mods imported.</returns>
        public virtual int Import(string textFile, ModSortTypes sortBy)
        {
            return this.Import(textFile, sortBy, null);
        }

        /// <summary>
        /// Imports IMod objects from files in a specified directory.
        /// </summary>
        /// <param name="dir">The directory to import files from.</param>
        /// <param name="searchPattern">The search pattern to find mods based on.</param>
        /// <param name="sortBy">The SortType enum describing how to sort the imported IMods.</param>
        /// <returns>An integer indicating the number of mods imported.</returns>
        public virtual int Import(string dir, string searchPattern, ModSortTypes sortBy)
        {
            if ((!Directory.Exists(dir)) || dir == null)
            {
                throw new DirectoryNotFoundException("Import directory not found");
            }

            List<T> added = new List<T>();
            string[] filePaths = Directory.GetFiles(dir, searchPattern, SearchOption.TopDirectoryOnly);
            int imported = 0;
            lock (this.Items)
            {
                foreach (string filePath in filePaths)
                {
                    T mod = (T)this.Owner.CreateMod();
                    mod.Id = Path.GetFileName(filePath);
                    mod.File = filePath;
                    mod.Name = Path.GetFileNameWithoutExtension(filePath);
                    this.Add(mod);
                    added.Add(mod);
                    imported++;
                }

                this.SortMods(sortBy, dir);
            }

            this.OnModListImported(new EventArgs());
            return imported;
        }

        /// <summary>
        /// Imports IMod objects from a text file of Filenames, supplementing the other information based on real files in the provided directory.
        /// </summary>
        /// <param name="textFile">The file containing a delimited list of mod filenames to import.</param>
        /// <param name="sortBy">A SortType enum indicating how to sort the imported IMod objects.</param>
        /// <param name="dir">The system directory containing mod files to get other information about each mod from.</param>
        /// <returns>An integer indicating the number of mods imported.</returns>
        public virtual int Import(string textFile, ModSortTypes sortBy, string dir)
        {
            int imported = 0;
            if (!File.Exists(textFile))
            {
                throw new FileNotFoundException("Import file " + textFile + " not found", textFile);
            }

            string[] lines = File.ReadAllLines(textFile);
            List<T> added = new List<T>();
            lock (this.Items)
            {
                foreach (string line in lines)
                {
                    T mod = (T)this.Owner.CreateMod();
                    string file = null;
                    if (dir != null)
                    {
                        file = dir + "\\" + line;
                    }
                    else
                    {
                        file = line;
                    }

                    mod.Id = line;
                    mod.Name = Path.GetFileNameWithoutExtension(line);
                    mod.File = file;
                    this.Add(mod);
                    added.Add(mod);
                    imported++;
                }

                if (dir != null)
                {
                    this.SortMods(sortBy, dir);
                }
            }

            this.OnModListImported(new EventArgs());
            return imported;
        }

        /// <summary>
        /// Inserts the provided IMod object after the other provided IMod object.
        /// </summary>
        /// <param name="thisMod">THe IMod object to insert.</param>
        /// <param name="otherMod">The IMod object to insert the mod after</param>
        /// <returns>The index the mod was inserted at.</returns>
        public int InsertAfter(T thisMod, T otherMod)
        {
            int index = this.IndexOf(otherMod);
            this.Insert(index + 1, thisMod);
            return index + 1;
        }

        /// <summary>
        /// Inserts the provided IMod object before the other provided IMod object.
        /// </summary>
        /// <param name="thisMod">The IMod object to insert.</param>
        /// <param name="otherMod">The IMod object to insert the mod before.</param>
        /// <returns>The index the mod was inserted at.</returns>
        public int InsertBefore(T thisMod, T otherMod)
        {
            int index = this.IndexOf(otherMod);
            this.Insert(index, thisMod);
            return index;
        }

        /// <summary>
        /// <para>Determines whether this list contains a mod like the IMod object specified.</para>
        /// <para>This matches true if the Id matches, usually.</para>
        /// </summary>
        /// <param name="thisMod">The IMod object to check for.</param>
        /// <returns>A bool indicating whether the specified mod is contained in this collection</returns>
        public bool ContainsMod(T thisMod)
        {
            foreach (T mod in this)
            {
                if (thisMod.Id.Equals(mod.Id, StringComparison.CurrentCultureIgnoreCase))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Determines whether this list contains a mod having the specified Id
        /// </summary>
        /// <param name="id">The Id string to check for in the list</param>
        /// <returns>A value indicating whether the mod described by this id is contained within this collection</returns>
        public bool ContainsMod(string id)
        {
            foreach (T mod in this)
            {
                if (mod.Id.Equals(id, StringComparison.CurrentCultureIgnoreCase))
                {
                    return true;
                }
            }

            return false;
        }

        public bool ContainsPattern(string pattern)
        {
            pattern = (pattern.Substring(0, 1).Equals("@")) ? pattern.Substring(1) : Regex.Escape(pattern);
            Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
            foreach (T mod in this)
            {
                if (regex.IsMatch(mod.Id))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Removes a mod from this collection
        /// </summary>
        /// <param name="thisMod">The mod to remove from the collection</param>
        public void RemoveMod(T thisMod)
        {
            foreach (T mod in this)
            {
                if (thisMod.Id.Equals(mod.Id, StringComparison.CurrentCultureIgnoreCase))
                {
                    this.Remove(mod);
                    return;
                }
            }
        }

        /// <summary>
        /// Sorts mods using the default sort type and direction
        /// </summary>
        public virtual void SortMods()
        {
            this.SortMods(this.SortBy);
        }

        /// <summary>
        /// Sorts mods using the specified SortType enumeration value
        /// </summary>
        /// <param name="sortBy">A SortType enumeration indicating how to sort the mods.</param>
        public virtual void SortMods(ModSortTypes sortBy)
        {
            this.SortMods(sortBy, null);
        }

        /// <summary>
        /// Sorts mods using the specified SortType enumeration and the mod information in the provided system directory.
        /// </summary>
        /// <param name="sortBy">A SortType enumeration indicating how to sort the mods.</param>
        /// <param name="dir">A filesystem directory containing the mods to sort by</param>
        public virtual void SortMods(ModSortTypes sortBy, string dir)
        {
            lock (this.Items)
            {
                List<T> list = (List<T>)this.Items;
                switch (sortBy)
                {
                    case ModSortTypes.FileName:
                        list.Sort(new ModFileComparer<T>(ModFileComparisonType.FileName, false));
                        break;
                    case ModSortTypes.FileNameReverse:
                        list.Sort(new ModFileComparer<T>(ModFileComparisonType.FileName, true));
                        break;
                    case ModSortTypes.TimeModified:
                        if (dir == null)
                        {
                            list.Sort(new ModFileComparer<T>(ModFileComparisonType.TimeModified, false));
                        }
                        else
                        {
                            list.Sort(new ModFileComparer<T>(ModFileComparisonType.TimeModified, dir, false));
                        }

                        break;
                    case ModSortTypes.TimeModifiedReverse:
                        if (dir == null)
                        {
                            list.Sort(new ModFileComparer<T>(ModFileComparisonType.TimeModified, true));
                        }
                        else
                        {
                            list.Sort(new ModFileComparer<T>(ModFileComparisonType.TimeModified, dir, true));
                        }

                        break;
                }
            }

            this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
        }

        /// <summary>
        /// Returns a new array of IMod objects from the IMods in the list.
        /// </summary>
        /// <returns>The new array</returns>
        public T[] ToArray()
        {
            return this.Items.ToArray();
        }

        /// <summary>
        /// Makes a deep clone of the ModList, creating new (but equal) IMod objects representing the existing IMods in the list.
        /// </summary>
        /// <returns>The new IModList</returns>
        public virtual object Clone()
        {
            BinaryFormatter bf = new BinaryFormatter();
            MemoryStream ms = new MemoryStream();
            bf.Serialize(ms, this);
            ms.Flush();
            ms.Position = 0;
            return (IModCollection<T>)bf.Deserialize(ms);
        }

        /// <summary>
        /// <para>A shortcut to call instead of firing a ModListImported event.</para>
        /// <para>This checks to see that the event has listeners before throwing it.</para>
        /// </summary>
        /// <param name="eventArgs">The EventArgs to include with the event</param>
        protected void OnModListImported(EventArgs eventArgs)
        {
            if (this.ModListImported != null)
            {
                this.ModListImported(this, eventArgs);
            }
        }

        /// <summary>
        /// <para>A shortcut to call instead of firing a ModListExported event.</para>
        /// <para>This checks to see that the event has listeners before throwing it.</para>
        /// </summary>
        /// <param name="eventArgs">The EventArgs to include with the event</param>
        protected void OnModListExported(EventArgs eventArgs)
        {
            if (this.ModListExported != null)
            {
                this.ModListExported(this, eventArgs);
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////
        /// <summary>   Sets an owner. </summary>
        ///
        /// <remarks>   Ben McClure, 11/22/2010. </remarks>
        ///
        /// <param name="game"> The game. </param>
        ////////////////////////////////////////////////////////////////////////////////////////////////////
        public void SetOwner(IGame game)
        {
            Owner = (IGame<T>)game;
        }
    }
}